package parser;

import codegen.Generator;
import lexer.Scanner;
import symbols.Morpheme;
import symbols.Morpheme.Codes;

import java.io.IOException;
import java.util.Stack;

public class Parser {

    private final Scanner scanner;
    private final Generator generator;
    private Morpheme lookAhead;
    private BTypes currentType;
    private boolean errorFound = false;
    private String errorMessage = null;
    private Symboltable sTable;
    private Stack<BTypes> types;
    private Stack<Integer> tempRegister;
    private Stack<Codes> operation;

    public Parser(final Scanner scanner) throws IOException {
        this.scanner = scanner;
        this.sTable = new Symboltable();
        this.lookAhead = scanner.getNextMorphen();
        this.types = new Stack<BTypes>();
        this.tempRegister = new Stack<Integer>();
        this.generator = new Generator(this.sTable);
        this.operation = new Stack<>();
    }

    public String getError() {
        return this.errorMessage;
    }

    /**
     * Entry point recursive syntax analysis algorithm.
     */
    public boolean parse() throws IOException {
        try {
            check_program();
            check(Codes.eof);
        } catch (final IllegalStateException e) {
            return false;
        }
        return !this.errorFound;
    }

    /**
     * Retrieves whether the lookahead terminal symbol matches expected terminal
     * symbol.
     */
    private void check(final Codes m) throws IOException {
        if (m == this.lookAhead.getCode()) {
            this.lookAhead = scanner.getNextMorphen();
        } else {
            error(m, this.lookAhead.getCode());
        }
    }

    private void check_program() throws IOException {
        check(Codes.program);
        switch (this.lookAhead.getCode()) {
            case ident:
                check(Codes.ident);
                check(Codes.semi);
                check_dclrow();
                check(Codes.begin);
                check_stmrow();
                check(Codes.end);
                break;
            default:
                error(Codes.ident, this.lookAhead.getCode());
                break;
        }
        this.generator.generate_exit();
    }

    private void check_stmrow() throws IOException {
        check_stmt();
        check_stmtrowtail();
    }

    private void check_stmtrowtail() throws IOException {
        switch (this.lookAhead.getCode()) {
            case semi:
                check(Codes.semi);
                check_stmt();
                check_stmtrowtail();
                break;
        }
    }

    private void check_stmt() throws IOException {
        switch (this.lookAhead.getCode()) {
            case read:
                check_in();
                break;
            case print:
                check_print();
                break;
            case ident:
                check_assign();
                break;
            case ifStmt:
                check_cond();
                break;
            case whileSmt:
                check_loop();
                break;
            default:
                error(Codes.stmt, this.lookAhead.getCode());
                break;
        }
    }

    private void check_loop() throws IOException {
        check(Codes.whileSmt);
        final int jump = this.generator.getJumpCounter();
        final int jumpDest = this.generator.getInstructionCounter() * 2;
        this.generator.addJumpDest(jump, jumpDest);
        check_expression();
        // loop expr must return boolean
        if (this.types.peek() != BTypes.BBoolean)
            errorContext("Loop condition expression is not of boolean type",
                    this.lookAhead);
        final int condJump = this.generator.getJumpCounter();
        this.generator.generate_condJump(condJump);
        check(Codes.doStmt);
        check_stmrow();
        check(Codes.doneStmt);
        this.generator.generate_jump(jump);
        this.generator.addJumpDest(condJump, this.generator.getInstructionCounter() * 2);
    }

    private void check_cond() throws IOException {
        final int jump = this.generator.getJumpCounter();
        check_if(jump);
        check_else();
        check(Codes.fi);
        this.generator.addJumpDest(jump, this.generator.getInstructionCounter() * 2);
    }

    private void check_else() throws IOException {
        switch (this.lookAhead.getCode()) {
            case elseStmt:
                check(Codes.elseStmt);
                check_stmrow();
                break;
        }
    }

    private void check_if(final int jump) throws IOException {
        check(Codes.ifStmt);
        check_expression();
        // if expr must return boolean
        if (this.types.peek() != BTypes.BBoolean)
            errorContext("Condition expression is not of boolean type",
                    lookAhead);
        final int condJump = this.generator.getJumpCounter();
        this.generator.generate_condJump(condJump);
        check(Codes.then);
        check_stmrow();
        this.generator.generate_jump(jump);
        this.generator.addJumpDest(condJump, this.generator.getInstructionCounter() * 2);
    }

    private void check_assign() throws IOException {
        final Morpheme currentMorph = this.lookAhead;
        check(Codes.ident);
        // assign must have the same equal type at both sides
        if (!this.sTable.is_in(currentMorph.getText())) {
            errorContext("Variable " + currentMorph.getText()
                    + " assigned but not declared", currentMorph);
        } else {
            this.types.push(this.sTable.type_of(currentMorph.getText()));
        }

        check(Codes.assign);
        check_expression();

        final BTypes bt1 = this.types.pop();
        final BTypes bt2 = this.types.peek();

        if (bt1 != bt2) {
            errorContext("Type mismatch at assign operation", this.lookAhead);
        }
        this.generator.generate_assign(currentMorph.getText());
    }

    private void check_dclrow() throws IOException {
        check_dcl();
        check_dclrowtail();
    }

    private void check_dclrowtail() throws IOException {
        switch (this.lookAhead.getCode()) {
            case semi:
                check(Codes.semi);
                check_dcl();
                check_dclrowtail();
                break;
        }
    }

    private void check_dcl() throws IOException {
        check_type();
        Morpheme currMorph = this.lookAhead;
        check(Codes.ident);
        check_TypeDcl(currMorph);
        check_dcltail();
    }

    private void check_TypeDcl(final Morpheme currMorph) {
        // check if morpheme name is already taken
        if (!this.sTable.is_in(currMorph.getText())) {
            this.sTable.insert(currMorph.getText(), this.currentType);
        } else {
            errorContext("Variable " + currMorph.getText()
                    + " already defined!", currMorph);
        }
    }

    private void check_dcltail() throws IOException {
        switch (this.lookAhead.getCode()) {
            case comma:
                check(Codes.comma);
                final Morpheme current = this.lookAhead;
                check(Codes.ident);
                check_TypeDcl(current);
                check_dcltail();
                break;
        }
    }

    private void check_type() throws IOException {
        switch (this.lookAhead.getCode()) {
            case integer:
                check(Codes.integer);
                this.currentType = BTypes.BInteger;
                break;
            case bool:
                check(Codes.bool);
                this.currentType = BTypes.BBoolean;
                break;
            default:
                error(Codes.type, lookAhead.getCode());
        }
    }

    private void check_bconst() throws IOException {
        switch (this.lookAhead.getCode()) {
            case falseStmt:
                check(Codes.falseStmt);
                break;
            case trueStmt:
                check(Codes.trueStmt);
                break;
            default:
                error(this.lookAhead.getCode(), Codes.falseStmt);
        }
    }

    private void check_in() throws IOException {
        check(Codes.read);
        // Check for declaration
        if (this.sTable.is_in(this.lookAhead.getText())) {
            this.types.push(this.sTable.type_of(this.lookAhead.getText()));
        } else {
            errorContext("Variable found that was not declared", lookAhead);
        }
        final String ident = this.lookAhead.getText();
        check(Codes.ident);
        this.generator.generate_in(ident);
    }

    private void check_print() throws IOException {
        check(Codes.print);
        check_expression();
        this.generator.generate_print();
    }

    private void check_expression() throws IOException {
        check_fak();
        check_exprTail();
    }

    private void check_exprTail() throws IOException {
        final BTypes bt1;
        final BTypes bt2;
        switch (this.lookAhead.getCode()) {
            case plus: {
                check(Codes.plus);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.plus);
                check_fak();
                bt1 = this.types.pop();
                check_exprTail();
                bt2 = this.types.peek();

                // expr types must be the same
                if (bt1 != bt2) {
                    errorContext("Type mismatch at (+) operand", lookAhead);
                }
                break;
            }
            case score: {
                // not defined on boolean
                if (this.types.peek() == BTypes.BBoolean) {
                    errorContext("Operation (-) not defined on boolean operands",
                            lookAhead);
                }
                check(Codes.score);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.score);
                check_fak();

                bt1 = types.pop();
                check_exprTail();
                bt2 = this.types.peek();

                // expr this.types must be the same
                if (bt1 != bt2) {
                    errorContext("Type mismatch at (-) operand", lookAhead);
                }
            }
        }
    }

    private void check_fak() throws IOException {
        check_prime();
        check_faktail();
    }

    private void check_faktail() throws IOException {
        final BTypes bt1;
        final BTypes bt2;

        switch (lookAhead.getCode()) {
            case mult:
                check(Codes.mult);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.mult);
                bt1 = this.types.pop();
                check_fak();
                bt2 = this.types.peek();

                // type must be the same
                if (bt1 != bt2)
                    errorContext("Operand type mismatch at multiplication",
                            lookAhead);
                break;
            case slash:
                // not defined for boolean
                if (this.types.peek() == BTypes.BBoolean)
                    errorContext("Operation (/) not defined on Boolean operands",
                            lookAhead);
                check(Codes.slash);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.slash);
                bt1 = this.types.pop();
                check_fak();
                bt2 = this.types.peek();

                // this.types must be the same
                if (bt1 != bt2)
                    errorContext("Operand type mismatch at division", lookAhead);
                break;
        }
    }

    private void check_prime() throws IOException {
        switch (lookAhead.getCode()) {
            case score:
                check(Codes.score);
                check_op();
                if (this.types.peek() == BTypes.BBoolean) {
                    this.generator.generate_logneg();
                } else {
                    this.generator.generate_neg();
                }
                break;
            default:
                check_op();
                break;
        }
    }

    private void check_op() throws IOException {
        switch (this.lookAhead.getCode()) {
            case falseStmt:
                this.types.push(BTypes.BBoolean);
                check_bconst();
                this.generator.generate_constFalse();
                writeOperation();
                break;
            case trueStmt:
                this.types.push(BTypes.BBoolean);
                check_bconst();
                this.generator.generate_constTrue();
                writeOperation();
                break;
            case number:
                this.types.push(BTypes.BInteger);
                final Morpheme currentMorph = this.lookAhead;
                check(Codes.number);
                this.generator.generate_constValue(currentMorph.getNumber());
                writeOperation();
                break;
            case ident:
                // Check for declaration
                if (this.sTable.is_in(this.lookAhead.getText())) {
                    this.types.push(this.sTable.type_of(this.lookAhead.getText()));
                } else {
                    errorContext("Variable found that was not declared", lookAhead);
                }
                final Morpheme current = this.lookAhead;
                check(Codes.ident);
                this.generator.generate_load(current.getText());
                writeOperation();
                break;
            case openBracket:
                this.operation.push(Codes.openBracket);
                check(Codes.openBracket);
                check_opTail();
                writeOperation();
                break;
            default:
                error(Codes.op, this.lookAhead.getCode());
                break;
        }

    }

    private void writeOperation() {
        if (!this.operation.isEmpty() && (this.operation.peek() != Codes.openBracket)) {
            final int right = this.generator.generate_store();
            final int left = this.tempRegister.pop();

            switch (this.operation.pop()) {
                case plus: {
                    if (this.types.peek() == BTypes.BInteger) {
                        this.generator.generate_addition(left, right);
                    } else {
                        this.generator.generate_or(left, right);
                    }
                    break;
                }
                case score: {
                    this.generator.generate_subtraction(left, right);
                    break;
                }
                case mult: {
                    if (this.types.peek() == BTypes.BInteger) {
                        this.generator.generate_multiplication(left, right);
                    } else {
                        this.generator.generate_and(left, right);
                    }
                    break;
                }
                case slash: {
                    this.generator.generate_division(left, right);
                    break;
                }
                case equal: {
                    this.generator.generate_equal(left, right);
                    break;
                }
                case lessThan: {
                    this.generator.generate_less(left, right);
                    break;
                }
            }

            this.sTable.free(left);
            this.sTable.free(right);
        }
    }

    private void check_opTail() throws IOException {
        check_expression();
        check_comp();
        check(Codes.closeBracket);
        this.operation.pop();
    }

    private void check_comp() throws IOException {
        final BTypes bt1;
        final BTypes bt2;

        switch (lookAhead.getCode()) {
            case equal:
                bt1 = this.types.pop();
                check(Codes.equal);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.equal);
                check_expression();
                bt2 = this.types.pop();

                // this.types must be the same
                if (bt1 == bt2) {
                    this.types.push(BTypes.BBoolean);
                } else {
                    errorContext("Type mismatch at equation", lookAhead);
                }
                break;
            case lessThan:
                // not defined on boolean
                if (this.types.peek() == BTypes.BBoolean)
                    errorContext("Comparison on boolean operands not defined",
                            lookAhead);
                bt1 = this.types.pop();
                check(Codes.lessThan);
                this.tempRegister.push(this.generator.generate_store());
                this.operation.push(Codes.lessThan);
                check_expression();
                bt2 = this.types.pop();

                // this.types must be the same
                if (bt1 == bt2) {
                    this.types.push(BTypes.BBoolean);
                } else {
                    errorContext("Type mismatch at comparison", lookAhead);
                }
                break;
            default:
                // epsilon
                break;
        }
    }

    private void error(final Codes expected, final Codes found) {
        this.errorFound = true;
        final StringBuilder sb = new StringBuilder();
        sb.append("(");
        sb.append(lookAhead.getStart().getLine());
        sb.append(",");
        sb.append(lookAhead.getStart().getRow());
        sb.append(")\t\t ");
        sb.append("expected ");
        sb.append(expected);
        sb.append("\tfound ");
        sb.append(found);
        this.errorMessage = sb.toString();
        throw new IllegalStateException(this.errorMessage);
    }

    private void errorContext(final String message, Morpheme morph) {
        if (!this.errorFound) {
            this.errorFound = true;
            this.errorMessage = message + " " + morph.getPosition().toString();
        }
        throw new IllegalStateException(this.errorMessage);
    }

    public String getCode() {
        return this.generator.toString();
    }
}